luci-app-csshnpd: Add new package
authorChris Swan <[email protected]>
Mon, 23 Jun 2025 12:34:50 +0000 (13:34 +0100)
committerPaul Donald <[email protected]>
Mon, 7 Jul 2025 22:17:44 +0000 (00:17 +0200)
Adding LuCI web interface for csshnpd package

Signed-off-by: Chris Swan <[email protected]>
applications/luci-app-csshnpd/Makefile [new file with mode: 0644]
applications/luci-app-csshnpd/htdocs/luci-static/resources/view/sshnpd/config.js [new file with mode: 0644]
applications/luci-app-csshnpd/htdocs/luci-static/resources/view/sshnpd/enroll.js [new file with mode: 0644]
applications/luci-app-csshnpd/root/usr/share/luci/menu.d/luci-app-csshnpd.json [new file with mode: 0644]
applications/luci-app-csshnpd/root/usr/share/rpcd/acl.d/luci-app-csshnpd.json [new file with mode: 0644]

diff --git a/applications/luci-app-csshnpd/Makefile b/applications/luci-app-csshnpd/Makefile
new file mode 100644 (file)
index 0000000..9c06a4e
--- /dev/null
@@ -0,0 +1,15 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=luci-app-csshnpd
+PKG_LICENSE:=GPL-2.0
+PKG_MAINTAINER:=Chris Swan <[email protected]>
+PKG_RELEASE:=1
+
+LUCI_TITLE:=NoPorts Web UI
+LUCI_DESCRIPTION:=LuCI config app for NoPorts daemon (csshnpd)
+LUCI_DEPENDS:=+luci-base +csshnpd
+LUCI_PKGARCH:=all
+
+include ../../luci.mk
+
+# call BuildPackage - OpenWrt buildroot signature
diff --git a/applications/luci-app-csshnpd/htdocs/luci-static/resources/view/sshnpd/config.js b/applications/luci-app-csshnpd/htdocs/luci-static/resources/view/sshnpd/config.js
new file mode 100644 (file)
index 0000000..1e9855c
--- /dev/null
@@ -0,0 +1,88 @@
+'use strict';
+'require view';
+'require form';
+
+function validateAtsign(section_id, value) {
+       if (value.length < 1) {
+               return _('Must not be empty and should start with @ (e.g., "@a").');
+       }
+       return true;
+}
+
+function validateDevice(section_id, value) {
+       if (value.length < 1) {
+               return _('Must be at least one character long (e.g., "a").');
+       }
+       if (value.length == 1) {
+               if (!/^[a-z]+$/.test(value)) {
+                       return _('First character should be a lowercase letter (e.g., "a").');
+               } else {
+                       return true;
+               }
+       }
+       if (!/^[a-z][a-z0-9_-]+$/.test(value)) {
+               return _('Device names may contain a-z 0-9 _ or - (e.g., "my_thing1").');
+       }
+       if (value.length > 36) {
+               return _('Maximum device name length is 36 characters.');
+       }
+       return true;
+}
+
+function validateOTP(section_id, value) {
+       if (value.length != 6) {
+               return _('Must be six characters (e.g., "S3CR3T").');
+       }
+       return true;
+}
+
+function firstAt(section_id, value) {
+       if (value && !value.startsWith('@')) {
+               value = '@' + value; // Ensure @ at start
+       }
+       return this.super('write', [section_id, value]);
+}
+
+return view.extend({
+       render: function() {
+               let m, s, o;
+
+               m = new form.Map('sshnpd', _('NoPorts'),
+                       _('Daemon Configuration'));
+
+               s = m.section(form.TypedSection, 'sshnpd', _('sshnpd config'));
+               s.anonymous = true;
+
+               o = s.option(form.Value, 'atsign', _('Device atSign'),
+                       _('The device atSign e.g. @device'));
+               o.default = '@device';
+               o.validate = validateAtsign;
+               o.write = firstAt;
+
+               o = s.option(form.Value, 'manager', _('Manager atSign'),
+                       _('The manager atSign e.g. @manager'));
+               o.default = '@manager';
+               o.validate = validateAtsign;
+               o.write = firstAt;
+
+               o = s.option(form.Value, 'device', _('Device name'),
+                       _('The name for this device e.g. openwrt'));
+               o.default = 'openwrt';
+               o.validate = validateDevice;
+
+               s.option(form.Value, 'args', _('Additional arguments'),
+                       _('Further command line arguments for the NoPorts daemon'));
+
+               o = s.option(form.Value, 'otp', _('Enrollment OTP/SPP'),
+                       _('One Time Passcode (OTP) for device atSign enrollment'));
+               o.default = '000000';
+               o.validate = validateOTP;
+
+               o = s.option(form.Flag, 'enabled', _('Enabled'),
+                       _('Check here to enable the service'));
+               o.default = '1';
+               o.rmempty = false;
+
+               return m.render();
+       },
+});
diff --git a/applications/luci-app-csshnpd/htdocs/luci-static/resources/view/sshnpd/enroll.js b/applications/luci-app-csshnpd/htdocs/luci-static/resources/view/sshnpd/enroll.js
new file mode 100644 (file)
index 0000000..a97cefb
--- /dev/null
@@ -0,0 +1,97 @@
+'use strict';
+'require view';
+'require dom';
+'require fs';
+'require ui';
+'require uci';
+'require network';
+
+
+return view.extend({
+       handleCommand: function(exec, args) {
+               let buttons = document.querySelectorAll('.diag-action > .cbi-button');
+
+               for (let i = 0; i < buttons.length; i++)
+                       buttons[i].setAttribute('disabled', 'true');
+
+               return fs.exec(exec, args).then(function(res) {
+                       let out = document.querySelector('textarea');
+
+                       dom.content(out, [ res.stdout || '', res.stderr || '' ]);
+               }).catch(function(err) {
+                       ui.addNotification(null, E('p', [ err ]))
+               }).finally(function() {
+                       for (let i = 0; i < buttons.length; i++)
+                               buttons[i].removeAttribute('disabled');
+               });
+       },
+
+       handleEnroll: function() {
+               return this.handleCommand('at_enroll.sh', "");
+       },
+
+       load: function() {
+               return uci.load('sshnpd').then(function() {
+                       let atsign = uci.get_first('sshnpd','','atsign'),
+                               keyfile = '/root/.atsign/keys/'+atsign+'_key.atKeys';
+                               return L.resolveDefault(fs.stat(keyfile), {});
+               });
+       },
+
+       render: function(res) {
+
+               const has_atkey = res.path;
+               const atsign = uci.get_first('sshnpd','','atsign');
+               const device = uci.get_first('sshnpd','','device');
+               const otp = uci.get_first('sshnpd','','otp');
+               const enrollready = atsign && device && otp && !has_atkey;
+
+               const instructions = E('div', { 'class': 'cbi-map-descr'}, _('Press the Enroll button then run this command on a system where '+atsign+' is activated:'));
+
+               const enrollcmd = E('code','at_activate approve -a '+atsign+' --arx noports --drx '+device);
+
+               let table = E('table', { 'class': 'table' }, [
+                               E('tr', { 'class': 'tr' }, [
+                                       E('td', { 'class': 'td left' }, [
+                                               E('span', { 'class': 'diag-action' }, [
+                                                       E('button', {
+                                                               'class': 'cbi-button cbi-button-action',
+                                                               'click': ui.createHandlerFn(this, 'handleEnroll')
+                                                       }, [ _('Enroll') ])
+                                               ])
+                                       ]),
+                               ])
+                       ]);
+
+               const cmdwindow = E('div', {'class': 'cbi-section'}, [
+                       E('div', { 'id' : 'command-output'},
+                               E('textarea', {
+                                       'id': 'widget.command-output',
+                                       'style': 'width: 100%; font-family:monospace; white-space:pre',
+                                       'readonly': true,
+                                       'wrap': 'on',
+                                       'rows': '20'
+                               })
+                       )
+                       ]);
+
+               let view = E('div', { 'class': 'cbi-map'}, [
+                       E('h2', {}, [ _('NoPorts atSign Enrollment') ]),
+                       atsign ? E([]) : E('div', { 'class': 'cbi-map-descr'}, _('atSign must be configured')),
+                       device ? E([]) : E('div', { 'class': 'cbi-map-descr'}, _('Device must be configured')),
+                       otp ? E([]) : E('div', { 'class': 'cbi-map-descr'}, _('OTP must be configured. An OTP can be generated using:')),
+                       otp ? E([]) : E('code','at_activate otp -a '+atsign),
+                       has_atkey ? E('div', { 'class': 'cbi-map-descr'}, _('Existing key found at: '+has_atkey)) : E([]),
+                       enrollready ? instructions : E([]),
+                       enrollready ? enrollcmd  : E([]),
+                       enrollready ? table : E([]),
+                       enrollready ? cmdwindow : E([]),
+               ]);
+
+               return view;
+       },
+
+       handleSaveApply: null,
+       handleSave: null,
+       handleReset: null
+});
diff --git a/applications/luci-app-csshnpd/root/usr/share/luci/menu.d/luci-app-csshnpd.json b/applications/luci-app-csshnpd/root/usr/share/luci/menu.d/luci-app-csshnpd.json
new file mode 100644 (file)
index 0000000..7d987bb
--- /dev/null
@@ -0,0 +1,30 @@
+{
+       "admin/network/sshnpd": {
+               "title": "NoPorts",
+               "order": 70,
+               "action": {
+                       "type": "firstchild"
+               },
+               "depends": {
+                       "acl": [ "luci-app-csshnpd" ]
+               }
+       },
+
+       "admin/network/sshnpd/config": {
+               "title": "NoPorts Config",
+               "order": 1,
+               "action": {
+                       "type": "view",
+                       "path": "sshnpd/config"
+               }
+       },
+
+       "admin/network/sshnpd/enroll": {
+               "title": "NoPorts Enrollment",
+               "order": 2,
+               "action": {
+                       "type": "view",
+                       "path": "sshnpd/enroll"
+               }
+       }
+}
diff --git a/applications/luci-app-csshnpd/root/usr/share/rpcd/acl.d/luci-app-csshnpd.json b/applications/luci-app-csshnpd/root/usr/share/rpcd/acl.d/luci-app-csshnpd.json
new file mode 100644 (file)
index 0000000..07054b5
--- /dev/null
@@ -0,0 +1,15 @@
+{
+       "luci-app-csshnpd": {
+               "description": "Grant UCI access for luci-app-csshnpd",
+               "read": {
+                       "uci": [ "sshnpd" ],
+                       "file": {
+                               "/usr/bin/at_enroll.sh": ["exec"],
+                               "/root/.atsign/keys/*": ["stat"]
+                       }
+               },
+               "write": {
+                       "uci": [ "sshnpd" ]
+               }
+       }
+}